<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Service
 * @subpackage Amazon_S3
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Stream.php 18951 2009-11-12 16:26:19Z alexander $
 */

/**
 * @see Zend_Service_Amazon_S3
 */
require_once 'Zend/Service/Amazon/S3.php';

/**
 * Amazon S3 PHP stream wrapper
 *
 * @category   Zend
 * @package    Zend_Service
 * @subpackage Amazon_S3
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Service_Amazon_S3_Stream
{
	/**
	 * @var boolean Write the buffer on fflush()?
	 */
	private $_writeBuffer = false;

	/**
	 * @var integer Current read/write position
	 */
	private $_position = 0;

	/**
	 * @var integer Total size of the object as returned by S3 (Content-length)
	 */
	private $_objectSize = 0;

	/**
	 * @var string File name to interact with
	 */
	private $_objectName = null;

	/**
	 * @var string Current read/write buffer
	 */
	private $_objectBuffer = null;

	/**
	 * @var array Available buckets
	 */
	private $_bucketList = array();

	/**
	 * @var Zend_Service_Amazon_S3
	 */
	private $_s3 = null;

	/**
	 * Retrieve client for this stream type
	 *
	 * @param  string $path
	 * @return Zend_Service_Amazon_S3
	 */
	protected function _getS3Client($path)
	{
		if ($this->_s3 === null) {
			$url = explode(':', $path);

			if (!$url) {
				/**
				 * @see Zend_Service_Amazon_S3_Exception
				 */
				require_once 'Zend/Service/Amazon/S3/Exception.php';
				throw new Zend_Service_Amazon_S3_Exception("Unable to parse URL $path");
			}

			$this->_s3 = Zend_Service_Amazon_S3::getWrapperClient($url[0]);
			if (!$this->_s3) {
				/**
				 * @see Zend_Service_Amazon_S3_Exception
				 */
				require_once 'Zend/Service/Amazon/S3/Exception.php';
				throw new Zend_Service_Amazon_S3_Exception("Unknown client for wrapper {$url[0]}");
			}
		}

		return $this->_s3;
	}

	/**
	 * Extract object name from URL
	 *
	 * @param string $path
	 * @return string
	 */
	protected function _getNamePart($path)
	{
		$url = parse_url($path);
		if ($url['host']) {
			return !empty($url['path']) ? $url['host'].$url['path'] : $url['host'];
		}

		return '';
	}
	/**
	 * Open the stream
	 *
	 * @param  string  $path
	 * @param  string  $mode
	 * @param  integer $options
	 * @param  string  $opened_path
	 * @return boolean
	 */
	public function stream_open($path, $mode, $options, $opened_path)
	{
		$name = $this->_getNamePart($path);
		// If we open the file for writing, just return true. Create the object
		// on fflush call
		if (strpbrk($mode, 'wax')) {
			$this->_objectName = $name;
			$this->_objectBuffer = null;
			$this->_objectSize = 0;
			$this->_position = 0;
			$this->_writeBuffer = true;
			$this->_getS3Client($path);
			return true;
		}
		else {
			// Otherwise, just see if the file exists or not
			$info = $this->_getS3Client($path)->getInfo($name);
			if ($info) {
				$this->_objectName = $name;
				$this->_objectBuffer = null;
				$this->_objectSize = $info['size'];
				$this->_position = 0;
				$this->_writeBuffer = false;
				$this->_getS3Client($path);
				return true;
			}
		}
		return false;
	}

	/**
	 * Close the stream
	 *
	 * @return void
	 */
	public function stream_close()
	{
		$this->_objectName = null;
		$this->_objectBuffer = null;
		$this->_objectSize = 0;
		$this->_position = 0;
		$this->_writeBuffer = false;
		unset($this->_s3);
	}

	/**
	 * Read from the stream
	 *
	 * @param  integer $count
	 * @return string
	 */
	public function stream_read($count)
	{
		if (!$this->_objectName) {
			return false;
		}

		$range_start = $this->_position;
		$range_end = $this->_position+$count;

		// Only fetch more data from S3 if we haven't fetched any data yet (postion=0)
		// OR, the range end position is greater than the size of the current object
		// buffer AND if the range end position is less than or equal to the object's
		// size returned by S3
		if (($this->_position == 0) || (($range_end > strlen($this->_objectBuffer)) && ($range_end <= $this->_objectSize))) {

			$headers = array(
                'Range' => "$range_start-$range_end"
			);

			$response = $this->_s3->_makeRequest('GET', $this->_objectName, null, $headers);

			if ($response->getStatus() == 200) {
				$this->_objectBuffer .= $response->getBody();
			}
		}

		$data = substr($this->_objectBuffer, $this->_position, $count);
		$this->_position += strlen($data);
		return $data;
	}

	/**
	 * Write to the stream
	 *
	 * @param  string $data
	 * @return integer
	 */
	public function stream_write($data)
	{
		if (!$this->_objectName) {
			return 0;
		}
		$len = strlen($data);
		$this->_objectBuffer .= $data;
		$this->_objectSize += $len;
		// TODO: handle current position for writing!
		return $len;
	}

	/**
	 * End of the stream?
	 *
	 * @return boolean
	 */
	public function stream_eof()
	{
		if (!$this->_objectName) {
			return true;
		}

		return ($this->_position >= $this->_objectSize);
	}

	/**
	 * What is the current read/write position of the stream
	 *
	 * @return integer
	 */
	public function stream_tell()
	{
		return $this->_position;
	}

	/**
	 * Update the read/write position of the stream
	 *
	 * @param  integer $offset
	 * @param  integer $whence
	 * @return boolean
	 */
	public function stream_seek($offset, $whence)
	{
		if (!$this->_objectName) {
			return false;
		}

		switch ($whence) {
			case SEEK_CUR:
				// Set position to current location plus $offset
				$new_pos = $this->_position + $offset;
				break;
			case SEEK_END:
				// Set position to end-of-file plus $offset
				$new_pos = $this->_objectSize + $offset;
				break;
			case SEEK_SET:
			default:
				// Set position equal to $offset
				$new_pos = $offset;
				break;
		}
		$ret = ($new_pos >= 0 && $new_pos <= $this->_objectSize);
		if ($ret) {
			$this->_position = $new_pos;
		}
		return $ret;
	}

	/**
	 * Flush current cached stream data to storage
	 *
	 * @return boolean
	 */
	public function stream_flush()
	{
		// If the stream wasn't opened for writing, just return false
		if (!$this->_writeBuffer) {
			return false;
		}

		$ret = $this->_s3->putObject($this->_objectName, $this->_objectBuffer);

		$this->_objectBuffer = null;

		return $ret;
	}

	/**
	 * Returns data array of stream variables
	 *
	 * @return array
	 */
	public function stream_stat()
	{
		if (!$this->_objectName) {
			return false;
		}

		$stat = array();
		$stat['dev'] = 0;
		$stat['ino'] = 0;
		$stat['mode'] = 0777;
		$stat['nlink'] = 0;
		$stat['uid'] = 0;
		$stat['gid'] = 0;
		$stat['rdev'] = 0;
		$stat['size'] = 0;
		$stat['atime'] = 0;
		$stat['mtime'] = 0;
		$stat['ctime'] = 0;
		$stat['blksize'] = 0;
		$stat['blocks'] = 0;

		if(($slash = strchr($this->_objectName, '/')) === false || $slash == strlen($this->_objectName)-1) {
			/* bucket */
			$stat['mode'] |= 040000;
		} else {
			$stat['mode'] |= 0100000;
		}
		$info = $this->_s3->getInfo($this->_objectName);
		if (!empty($info)) {
			$stat['size']  = $info['size'];
			$stat['atime'] = time();
			$stat['mtime'] = $info['mtime'];
		}

		return $stat;
	}

	/**
	 * Attempt to delete the item
	 *
	 * @param  string $path
	 * @return boolean
	 */
	public function unlink($path)
	{
		return $this->_getS3Client($path)->removeObject($this->_getNamePart($path));
	}

	/**
	 * Attempt to rename the item
	 *
	 * @param  string  $path_from
	 * @param  string  $path_to
	 * @return boolean False
	 */
	public function rename($path_from, $path_to)
	{
		// TODO: Renaming isn't supported, always return false
		return false;
	}

	/**
	 * Create a new directory
	 *
	 * @param  string  $path
	 * @param  integer $mode
	 * @param  integer $options
	 * @return boolean
	 */
	public function mkdir($path, $mode, $options)
	{
		return $this->_getS3Client($path)->createBucket(parse_url($path, PHP_URL_HOST));
	}

	/**
	 * Remove a directory
	 *
	 * @param  string  $path
	 * @param  integer $options
	 * @return boolean
	 */
	public function rmdir($path, $options)
	{
		return $this->_getS3Client($path)->removeBucket(parse_url($path, PHP_URL_HOST));
	}

	/**
	 * Attempt to open a directory
	 *
	 * @param  string $path
	 * @param  integer $options
	 * @return boolean
	 */
	public function dir_opendir($path, $options)
	{

		if (preg_match('@^([a-z0-9+.]|-)+://$@', $path)) {
			$this->_bucketList = $this->_getS3Client($path)->getBuckets();
		}
		else {
			$url = parse_url($path);
			$this->_bucketList = $this->_getS3Client($path)->getObjectsByBucket($url["host"]);
		}

		return ($this->_bucketList !== false);
	}

	/**
	 * Return array of URL variables
	 *
	 * @param  string $path
	 * @param  integer $flags
	 * @return array
	 */
	public function url_stat($path, $flags)
	{
		$stat = array();
		$stat['dev'] = 0;
		$stat['ino'] = 0;
		$stat['mode'] = 0777;
		$stat['nlink'] = 0;
		$stat['uid'] = 0;
		$stat['gid'] = 0;
		$stat['rdev'] = 0;
		$stat['size'] = 0;
		$stat['atime'] = 0;
		$stat['mtime'] = 0;
		$stat['ctime'] = 0;
		$stat['blksize'] = 0;
		$stat['blocks'] = 0;

		$name = $this->_getNamePart($path);
		if(($slash = strchr($name, '/')) === false || $slash == strlen($name)-1) {
			/* bucket */
			$stat['mode'] |= 040000;
		} else {
			$stat['mode'] |= 0100000;
		}
		$info = $this->_getS3Client($path)->getInfo($name);

		if (!empty($info)) {
			$stat['size']  = $info['size'];
			$stat['atime'] = time();
			$stat['mtime'] = $info['mtime'];
		}

		return $stat;
	}

	/**
	 * Return the next filename in the directory
	 *
	 * @return string
	 */
	public function dir_readdir()
	{
		$object = current($this->_bucketList);
		if ($object !== false) {
			next($this->_bucketList);
		}
		return $object;
	}

	/**
	 * Reset the directory pointer
	 *
	 * @return boolean True
	 */
	public function dir_rewinddir()
	{
		reset($this->_bucketList);
		return true;
	}

	/**
	 * Close a directory
	 *
	 * @return boolean True
	 */
	public function dir_closedir()
	{
		$this->_bucketList = array();
		return true;
	}
}
